package dryven.util;

import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;

import dryven.model.binding.fieldset.Field;
import dryven.model.binding.fieldset.FieldGroup;
import dryven.model.binding.fieldset.FieldSet;

public class CSVParser<T> {
	private FieldSet _fields;
	private char _separator;
	private Reader _reader;
	private Class<T> _modelType;
	private int _colCount = 0;
	private boolean _eof = false;
	
	
	public CSVParser(Class<T> modelType, FieldSet fields, char sep, Reader reader) {
		super();
		_modelType = modelType;
		_fields = fields;
		_reader = reader;
		_separator = sep;
		int typeCount = 0;
		for (Class<?> type : _fields.getModelTypes()) {
			if(type!=modelType || typeCount!=0) {
				throw new IllegalArgumentException();
			}
			++typeCount;
		}
		//init col count
		for(FieldGroup group : _fields.groups()) {
			for (Field f : group.getFields()) {
				++_colCount;
			}
		}
	}
	
	public Iterable<T> parse() {
		return new ParsingIterable();
	}
	
	class ParsingIterable implements Iterable<T> {

		@Override
		public Iterator<T> iterator() {
			return new ParsingIterator();
		}
		
	}
	
	class ParsingIterator implements Iterator<T> {

		private RowData row = null;
		private boolean nextChecked = false;
		@Override
		public boolean hasNext() {
			if(!nextChecked) {
				row = getNextRow();
				nextChecked = true;
			}
			return row!=null;
		}

		@Override
		public T next() {
			nextChecked = false;
			if(row==null) {
				return null;
			} else {
				T model = createNewModel();
				fillModel(row, model);
				return model;
			}
		}

		@Override
		public void remove() {
			throw new UnsupportedOperationException();
		}
		
	}
	
	private T createNewModel() {
		T model = null;
		try {
			model = _modelType.newInstance();
		} catch (Exception e) {
			throw new RuntimeException(String.format("%s does not have a default constructor", _modelType.getName()),e);
		}
		return model;
	}
	
	private RowData getNextRow() {
		if(_eof) {
			return null;
		}
		boolean inQuotes = false;
		int currentChar = 0;
		ArrayList<String> cols = new ArrayList<String>(_colCount);
		StringBuffer buf = new StringBuffer();
		boolean prevWasNewlineChar = false;
		boolean prevWasEscapeChar = false;
		try {
			//TODO: cleanup in case there is no last empty line
			while(true) {
				currentChar=_reader.read();
				if(currentChar=='\\' && inQuotes) {
					prevWasNewlineChar = false;
					//of two consecutive backslashes, the second does not count as an escape char
					if(prevWasEscapeChar) {
						buf.append((char)currentChar);
					}
					prevWasEscapeChar = !prevWasEscapeChar;
				} else if(currentChar=='"' && !prevWasEscapeChar) {
					prevWasNewlineChar = false;
					prevWasEscapeChar = false;
					inQuotes=!inQuotes;
				} else if(!inQuotes && (currentChar==_separator || currentChar=='\n' || currentChar=='\r' || currentChar==-1)) {
					if(prevWasNewlineChar && currentChar!=_separator) {
						continue;
					}
					prevWasNewlineChar = true;
					prevWasEscapeChar = false;
					cols.add(buf.toString());
					buf = new StringBuffer();
					if(currentChar=='\n' || currentChar=='\r' || currentChar==-1) {
						if(currentChar==-1) {
							_eof = true;
						}
						break;
					}
				}
				else {
					prevWasNewlineChar = false;
					prevWasEscapeChar = false;
					buf.append((char)currentChar);
				}
			}
			if(cols.size()==0) {
				return null;
			} else {
				return new RowData(cols);
			}
		} catch (IOException e) {
			throw new RuntimeException(e);
		}
	}
	
	private void fillModel(RowData data, T model) {
		//TODO: cast types to appropriate type and don't assume it's String
		String col = null;
		for (Field f : _fields.getDefaultGroup().getFields()) {
			if(data.hasMoreColumns()) {
				col = data.getNextColumn();
				f.setValue(model, col);
			} else {
				break;
			}
		}
	}
}

class RowData {
	private ArrayList<String> _rows;
	private int _index;
	public RowData(ArrayList<String> rows) {
		super();
		_rows = rows;
		_index = 0;
	}
	
	public boolean hasMoreColumns() {
		return _index<_rows.size();
	}
	
	public String getNextColumn() {
		String result = _rows.get(_index);
		++_index;
		return result;
	}
}